(async function() {
  const STORAGE_KEY = 'todo-web.items';
  const STATUS = ['待分配','进行中','待测试','待更新','待通知用户','已完成'];
  const TYPES = ['日常','长期'];

  const $input = document.getElementById('todo-input');
  const $owner = document.getElementById('todo-owner');
  const $assignedAt = document.getElementById('todo-assignedAt');
  const $progressSel = document.getElementById('todo-progress');
  const $typeSel = document.getElementById('todo-type');
  const $addBtn = document.getElementById('add-btn');

  const $list = document.getElementById('todo-list');
  const $itemsLeft = document.getElementById('items-left');
  const $clearCompleted = document.getElementById('clear-completed');
  const $filters = document.querySelectorAll('.filter');
  const $progressFilter = document.getElementById('progress-filter');
  const $typeFilter = document.getElementById('type-filter');
  const $announce = document.getElementById('announce');
  const $exportBtn = document.getElementById('export-btn');
  const $importBtn = document.getElementById('import-btn');
  const $importFile = document.getElementById('import-file');
  // 备份管理 & 同步 UI
  const $backupList = document.getElementById('backup-list');
  const $backupRefresh = document.getElementById('backup-refresh');
  const $backupWriteNow = document.getElementById('backup-write-now');
  const $syncUrl = document.getElementById('sync-url');
  // 字体选择（平台自动过滤与持久化 + 自定义字体）
  const $fontSel = document.getElementById('app-font');
  const $fontUrl = document.getElementById('font-url');
  const $fontUrlLoad = document.getElementById('font-url-load');
  const $fontFile = document.getElementById('font-file');
  const FONT_STORAGE_KEY = 'todo-web.font';
  const FONT_CUSTOM_URL_KEY = 'todo-web.font.custom.url';
  const CUSTOM_FONT_NAME = '__CustomFont__';
  let customFontLoaded = false;

  function detectPlatform() {
    const ua = navigator.userAgent.toLowerCase();
    const pf = (navigator.platform || '').toLowerCase();
    if (pf.includes('win') || ua.includes('windows')) return 'win';
    if (pf.includes('mac') || ua.includes('mac os')) return 'mac';
    if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod')) return 'ios';
    if (ua.includes('android')) return 'android';
    return 'linux';
  }
  const FONTS = [
    { id: 'system',   label: '系统默认', stack: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans','Droid Sans','Helvetica Neue', Arial, 'Noto Sans','PingFang SC','Microsoft YaHei', sans-serif", platforms: ['win','mac','linux','ios','android'] },
    { id: 'yahei',    label: '微软雅黑', stack: "'Microsoft YaHei','Segoe UI',Arial,sans-serif", platforms: ['win'] },
    { id: 'segoe',    label: 'Segoe UI', stack: "'Segoe UI','Microsoft YaHei',Arial,sans-serif", platforms: ['win'] },
    { id: 'pingfang', label: '苹方（PingFang SC）', stack: "'PingFang SC','Hiragino Sans GB','Microsoft YaHei',Arial,sans-serif", platforms: ['mac','ios'] },
    { id: 'sf',       label: 'SF Pro（系统）', stack: "-apple-system, 'SF Pro Text','SF Pro Display','Helvetica Neue', Arial, 'PingFang SC','Hiragino Sans GB','Microsoft YaHei', sans-serif", platforms: ['mac','ios'] },
    { id: 'sourcehan',label: '思源黑体（Source Han Sans SC）', stack: "'Source Han Sans SC','Microsoft YaHei','PingFang SC','Noto Sans',Arial,sans-serif", platforms: ['win','mac','linux'] },
    { id: 'noto',     label: 'Noto Sans', stack: "'Noto Sans','Microsoft YaHei','PingFang SC',Arial,sans-serif", platforms: ['win','mac','linux','android'] },
    { id: 'ubuntu',   label: 'Ubuntu', stack: "Ubuntu, 'Noto Sans', Arial, sans-serif", platforms: ['linux'] },
    { id: 'cantarell',label: 'Cantarell', stack: "Cantarell, 'Noto Sans', Arial, sans-serif", platforms: ['linux'] },
    { id: 'roboto',   label: 'Roboto', stack: "Roboto, 'Noto Sans', Arial, sans-serif", platforms: ['android'] },
    { id: 'harmony',  label: 'HarmonyOS Sans', stack: "'HarmonyOS Sans','Noto Sans', Arial, sans-serif", platforms: ['android'] },
  ];
  const DEFAULT_BY_PLATFORM = { win: 'yahei', mac: 'pingfang', ios: 'pingfang', linux: 'noto', android: 'roboto' };
  let currentFonts = [];

  function fallbackStackForPlatform(pf) {
    const defId = DEFAULT_BY_PLATFORM[pf] || 'system';
    const def = FONTS.find(f => f.id === defId) || FONTS[0];
    return def.stack;
  }

  function applyFont(id) {
    const pf = detectPlatform();
    if (id === 'custom') {
      document.body.style.fontFamily = `'${CUSTOM_FONT_NAME}', ${fallbackStackForPlatform(pf)}`;
      return;
    }
    const sel = (currentFonts.find(f => f.id === id) || FONTS.find(f => f.id === id) || FONTS.find(f => f.id === 'system'));
    document.body.style.fontFamily = sel.stack;
  }

  function buildFontOptions() {
    if (!$fontSel) return;
    const pf = detectPlatform();
    currentFonts = FONTS.filter(f => f.platforms.includes(pf));
    $fontSel.innerHTML = '';
    for (const f of currentFonts) {
      const opt = document.createElement('option');
      opt.value = f.id; opt.textContent = f.label;
      $fontSel.appendChild(opt);
    }
    // 如已存在自定义字体（已加载或有持久化URL），追加自定义项
    const persistedUrl = localStorage.getItem(FONT_CUSTOM_URL_KEY);
    if (customFontLoaded || persistedUrl) {
      const opt = document.createElement('option');
      opt.value = 'custom'; opt.textContent = '自定义字体';
      $fontSel.appendChild(opt);
    }
  }

  async function loadCustomFontFromUrl(url) {
    try {
      const ff = new FontFace(CUSTOM_FONT_NAME, `url(${url})`);
      await ff.load();
      document.fonts.add(ff);
      customFontLoaded = true;
      localStorage.setItem(FONT_CUSTOM_URL_KEY, url);
      buildFontOptions();
      if ($fontSel) { $fontSel.value = 'custom'; }
      localStorage.setItem(FONT_STORAGE_KEY, 'custom');
      applyFont('custom');
    } catch (e) {
      console.error('自定义字体加载失败(URL):', e);
      alert('自定义字体加载失败，请检查URL或文件格式');
    }
  }

  async function loadCustomFontFromFile(file) {
    try {
      const blobUrl = URL.createObjectURL(file);
      const ff = new FontFace(CUSTOM_FONT_NAME, `url(${blobUrl})`);
      await ff.load();
      document.fonts.add(ff);
      customFontLoaded = true;
      // 文件来源仅在当前会话有效，不做持久化
      buildFontOptions();
      if ($fontSel) { $fontSel.value = 'custom'; }
      localStorage.setItem(FONT_STORAGE_KEY, 'custom');
      applyFont('custom');
    } catch (e) {
      console.error('自定义字体加载失败(文件):', e);
      alert('自定义字体加载失败，请选择有效的字体文件');
    }
  }

  // 初始化
  buildFontOptions();
  const pf = detectPlatform();
  let initialFont = localStorage.getItem(FONT_STORAGE_KEY);
  const availableIds = () => currentFonts.map(f => f.id);
  const persistedUrl = localStorage.getItem(FONT_CUSTOM_URL_KEY) || '';
  if (initialFont === 'custom' && persistedUrl) {
    // 先尝试加载自定义URL字体，再应用
    loadCustomFontFromUrl(persistedUrl).then(() => {
      if ($fontSel) $fontSel.value = 'custom';
    }).catch(() => {
      // 回退到平台默认
      initialFont = DEFAULT_BY_PLATFORM[pf] || 'system';
      localStorage.setItem(FONT_STORAGE_KEY, initialFont);
      applyFont(initialFont);
      if ($fontSel) $fontSel.value = initialFont;
    });
  } else {
    if (!initialFont || !availableIds().includes(initialFont)) {
      initialFont = DEFAULT_BY_PLATFORM[pf] || 'system';
      localStorage.setItem(FONT_STORAGE_KEY, initialFont);
    }
    applyFont(initialFont);
    if ($fontSel) $fontSel.value = initialFont;
  }

  // 事件绑定
  if ($fontSel) {
    $fontSel.addEventListener('change', () => {
      const v = $fontSel.value;
      localStorage.setItem(FONT_STORAGE_KEY, v);
      applyFont(v);
    });
  }
  if ($fontUrlLoad) {
    $fontUrlLoad.addEventListener('click', () => {
      const url = ($fontUrl && $fontUrl.value || '').trim();
      if (!url) { alert('请输入字体文件URL'); return; }
      loadCustomFontFromUrl(url);
    });
  }
  if ($fontFile) {
    $fontFile.addEventListener('change', (e) => {
      const f = e.target.files && e.target.files[0];
      if (!f) return;
      loadCustomFontFromFile(f);
    });
  }
  const $syncToken = document.getElementById('sync-token');
  const $syncAuto = document.getElementById('sync-auto');
  const $syncPush = document.getElementById('sync-push');
  const $syncPull = document.getElementById('sync-pull');
  const $syncStatus = document.getElementById('sync-status');
  const $toastContainer = document.getElementById('toast-container');

  // IndexedDB & backup
  let db = null;
  let useIDB = false;
  let autoBackupTimer = null;
  let backupDebounce = null;
  let syncPushDebounce = null;
  let skipNextSyncPush = false;
  const AUTO_BACKUP_INTERVAL_MS = 5 * 60 * 1000;
  const AUTO_BACKUP_KEY = 'todo-web.autoBackup';
  const KV_ITEMS_KEY = 'items';
  const SYNC_URL_KEY = 'todo-web.syncUrl';
  const SYNC_TOKEN_KEY = 'todo-web.syncToken';
  const SYNC_AUTO_KEY = 'todo-web.syncAuto';
  const autoSaveBlock = new Set();

  let items = [];
  let currentFilter = 'active';
  let progressFilter = 'all';
  let typeFilter = 'all';
  let draggedId = null;

  function loadItems() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? parsed.map(i => ({
        id: i.id || uid(),
        text: i.text || '',
        completed: !!i.completed,
        createdAt: i.createdAt || Date.now(),
        owner: i.owner || '',
        assignedAt: i.assignedAt ?? null,
        progress: STATUS.includes(i.progress) ? i.progress : (i.completed ? '已完成' : '待分配'),
        type: TYPES.includes(i.type) ? i.type : '日常',
      })) : [];
    } catch {
      return [];
    }
  }

  function saveItems() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
    if (useIDB) { idbPut(KV_ITEMS_KEY, items); }
    // 取消自动备份：不再触发 debouncedBackup()
    if ($syncAuto && $syncAuto.checked && !skipNextSyncPush) { debouncedSyncPush(); }
    skipNextSyncPush = false;
  }
  function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2,8); }

  // IndexedDB helpers & backup
  async function openDB() {
    return new Promise((resolve, reject) => {
      try {
        const req = indexedDB.open('todo-web', 1);
        req.onupgradeneeded = () => {
          const idb = req.result;
          if (!idb.objectStoreNames.contains('kv')) idb.createObjectStore('kv');
          if (!idb.objectStoreNames.contains('backups')) idb.createObjectStore('backups', { keyPath: 'id', autoIncrement: true });
        };
        req.onsuccess = () => resolve(req.result);
        req.onerror = () => reject(req.error);
      } catch (err) { reject(err); }
    });
  }
  async function idbGet(key) {
    return new Promise((resolve) => {
      try {
        const tx = db.transaction('kv', 'readonly');
        const store = tx.objectStore('kv');
        const r = store.get(key);
        r.onsuccess = () => resolve(r.result || null);
        r.onerror = () => resolve(null);
      } catch { resolve(null); }
    });
  }
  async function idbPut(key, value) {
    return new Promise((resolve) => {
      try {
        const tx = db.transaction('kv', 'readwrite');
        const store = tx.objectStore('kv');
        const r = store.put(value, key);
        r.onsuccess = () => resolve(true);
        r.onerror = () => resolve(false);
      } catch { resolve(false); }
    });
  }
  async function idbAddBackup(snapshot) {
    return new Promise((resolve) => {
      try {
        const tx = db.transaction('backups', 'readwrite');
        const store = tx.objectStore('backups');
        const r = store.add(snapshot);
        r.onsuccess = () => resolve(true);
        r.onerror = () => resolve(false);
      } catch { resolve(false); }
    });
  }
  async function idbGetAllBackups() {
    return new Promise((resolve) => {
      try {
        const tx = db.transaction('backups', 'readonly');
        const store = tx.objectStore('backups');
        const r = store.getAll();
        r.onsuccess = () => resolve(Array.isArray(r.result) ? r.result : []);
        r.onerror = () => resolve([]);
      } catch { resolve([]); }
    });
  }
  async function idbDeleteBackup(id) {
    return new Promise((resolve) => {
      try {
        const tx = db.transaction('backups', 'readwrite');
        const store = tx.objectStore('backups');
        const r = store.delete(id);
        r.onsuccess = () => resolve(true);
        r.onerror = () => resolve(false);
      } catch { resolve(false); }
    });
  }
  function normalizeItems(parsed) {
    return Array.isArray(parsed) ? parsed.map(i => ({
      id: i.id || uid(),
      text: i.text || '',
      completed: !!i.completed,
      createdAt: i.createdAt || Date.now(),
      owner: i.owner || '',
      assignedAt: i.assignedAt ?? null,
      progress: STATUS.includes(i.progress) ? i.progress : (i.completed ? '已完成' : '待分配'),
      type: TYPES.includes(i.type) ? i.type : '日常',
    })) : [];
  }
  async function initStorage() {
    try { db = await openDB(); useIDB = !!db; } catch { useIDB = false; }
  }
  async function loadItemsAsync() {
    if (useIDB) {
      const data = await idbGet(KV_ITEMS_KEY);
      const normalized = normalizeItems(data || []);
      if (normalized.length) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized));
        return normalized;
      }
    }
    return loadItems();
  }
  function debouncedBackup() { /* 自动备份取消 */ }
  function scheduleAutoBackup() { /* 自动备份取消 */ }
  async function writeBackupSnapshot(manual = false) {
    if (!manual) return;
    const snapshot = { ts: Date.now(), items };
    if (useIDB) { await idbAddBackup(snapshot); }
    else {
      try { localStorage.setItem(AUTO_BACKUP_KEY, JSON.stringify(snapshot)); } catch {}
    }
  }
  async function listBackups() {
    if (useIDB) {
      const arr = await idbGetAllBackups();
      return arr.sort((a,b) => b.ts - a.ts);
    }
    try { const raw = localStorage.getItem(AUTO_BACKUP_KEY); const s = raw ? JSON.parse(raw) : null; return s ? [s] : []; } catch { return []; }
  }
  async function renderBackups() {
    if (!$backupList) return;
    const backups = await listBackups();
    $backupList.innerHTML = '';
    for (const b of backups) {
      const li = document.createElement('li');
      const meta = document.createElement('span');
      meta.className = 'meta';
      const time = new Date(b.ts).toLocaleString('zh-CN', { hour12: false });
      const count = Array.isArray(b.items) ? b.items.length : 0;
      meta.textContent = `备份时间：${time} · 条目数：${count}`;
      const actions = document.createElement('span');
      actions.className = 'actions';
      const btnRestore = document.createElement('button'); btnRestore.textContent = '恢复';
      const btnExport = document.createElement('button'); btnExport.textContent = '导出';
      const btnDelete = document.createElement('button'); btnDelete.textContent = '删除';
      actions.appendChild(btnRestore); actions.appendChild(btnExport); actions.appendChild(btnDelete);
      li.appendChild(meta); li.appendChild(actions);
      $backupList.appendChild(li);
      btnRestore.addEventListener('click', async () => { await restoreBackup(b); });
      btnExport.addEventListener('click', () => exportBackup(b));
      btnDelete.addEventListener('click', async () => { await deleteBackup(b); renderBackups(); });
    }
  }
  async function restoreBackup(b) {
    const normalized = normalizeItems(b.items || []);
    items = normalized;
    skipNextSyncPush = true; // 恢复不触发同步推送
    saveItems();
    render();
    announce('已从备份恢复');
  }
  async function deleteBackup(b) {
    if (useIDB && typeof b.id !== 'undefined') { await idbDeleteBackup(b.id); }
    else { try { localStorage.removeItem(AUTO_BACKUP_KEY); } catch {} }
    announce('已删除备份');
  }
  function exportBackup(b) {
    const payload = { version: 1, backup: b };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    const ts = new Date(b.ts || Date.now()).toISOString().replace(/[:.]/g, '-');
    a.download = `backup-${ts}.json`;
    a.href = URL.createObjectURL(blob);
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 1000);
    announce('已导出备份');
  }

  // 跨设备同步
  function setSyncStatus(msg) { if ($syncStatus) $syncStatus.textContent = msg; }
  function loadSyncConfig() {
    if ($syncUrl) $syncUrl.value = localStorage.getItem(SYNC_URL_KEY) || '';
    if ($syncToken) $syncToken.value = localStorage.getItem(SYNC_TOKEN_KEY) || '';
    if ($syncAuto) $syncAuto.checked = localStorage.getItem(SYNC_AUTO_KEY) === '1';
  }
  function saveSyncConfig() {
    if ($syncUrl) localStorage.setItem(SYNC_URL_KEY, $syncUrl.value.trim());
    if ($syncToken) localStorage.setItem(SYNC_TOKEN_KEY, $syncToken.value.trim());
    if ($syncAuto) localStorage.setItem(SYNC_AUTO_KEY, $syncAuto.checked ? '1' : '0');
  }
  function debouncedSyncPush() { clearTimeout(syncPushDebounce); syncPushDebounce = setTimeout(pushSync, 2000); }
  // 登录与鉴权逻辑
  const auth = { user: null, pass: null, remember: false };
  function loadAuthFromStorage() {
    try {
      const u = localStorage.getItem('auth.user');
      const p = localStorage.getItem('auth.pass');
      auth.user = u || null;
      auth.pass = p || null;
      auth.remember = !!(u && p);
    } catch (e) {}
  }
  function saveAuthToStorage() {
    if (auth.remember && auth.user && auth.pass) {
      localStorage.setItem('auth.user', auth.user);
      localStorage.setItem('auth.pass', auth.pass);
    }
  }
  function clearAuth() {
    auth.user = null; auth.pass = null; auth.remember = false;
    localStorage.removeItem('auth.user');
    localStorage.removeItem('auth.pass');
  }
  function toBase64Utf8(str) {
    try { return btoa(unescape(encodeURIComponent(str))); } catch { return btoa(str); }
  }
  function getAuthHeader() {
    if (auth.user && auth.pass) {
      const token = toBase64Utf8(`${auth.user}:${auth.pass}`);
      return `Basic ${token}`;
    }
    // 兼容旧的 Token 方式（修正为 $syncToken）
    if (typeof $syncToken !== 'undefined' && $syncToken && $syncToken.value && $syncToken.value.trim()) {
      return `Bearer ${$syncToken.value.trim()}`;
    }
    return null;
  }
  function updateTopbar() {
    const badge = document.getElementById('user-badge');
    const logoutBtn = document.getElementById('logout-btn');
    if (!badge || !logoutBtn) return;
    if (auth.user) {
      badge.textContent = auth.user;
      badge.style.display = 'inline-block';
      logoutBtn.style.display = 'inline-block';
    } else {
      badge.textContent = '未登录';
      badge.style.display = 'none';
      logoutBtn.style.display = 'none';
    }
  }
  function showLogin() {
    const overlay = document.getElementById('login-overlay');
    if (overlay) overlay.classList.add('show');
  }
  function hideLogin() {
    const overlay = document.getElementById('login-overlay');
    if (overlay) overlay.classList.remove('show');
  }
  
  // 绑定登录事件
  (function bindLoginEvents(){
    const submit = document.getElementById('login-submit');
    const logout = document.getElementById('logout-btn');
    const form = document.getElementById('login-form');
    if (form) {
      form.addEventListener('submit', (e) => {
        e.preventDefault();
        submit?.click();
      });
    }
    if (submit) {
      submit.addEventListener('click', async () => {
        const u = (document.getElementById('login-username')?.value || '').trim();
        const p = document.getElementById('login-password')?.value || '';
        const remember = !!document.getElementById('login-remember')?.checked;
        if (!u || !p) {
          alert('请输入用户名和密码');
          return;
        }
        auth.user = u; auth.pass = p; auth.remember = remember;
        if (remember) saveAuthToStorage();
        updateTopbar();
        hideLogin();
        const urlVal = ($syncUrl && $syncUrl.value && $syncUrl.value.trim()) || '';
        if (urlVal) {
          setSyncStatus('登录成功，正在拉取...');
          try { await pullSync(); } catch (e) {}
        }
      });
    }
    if (logout) {
      logout.addEventListener('click', () => {
        clearAuth();
        updateTopbar();
        showLogin();
      });
    }
  })();
  
  // 初始化鉴权状态
  (function initAuth(){
    loadAuthFromStorage();
    updateTopbar();
    try {
      const urlVal = (typeof $syncUrl !== 'undefined' && $syncUrl && $syncUrl.value) ? $syncUrl.value.trim() : '';
      if (!auth.user || !auth.pass) {
        if (urlVal) showLogin();
      } else if (urlVal) {
        // 已有凭据且配置了 URL，自动拉取一次
        setSyncStatus('已登录，正在拉取云端数据...');
        // 无需 await，避免阻塞初始化
        pullSync();
      }
    } catch(e) {}
  })();
  
  // 修改同步请求头为支持 Basic/Token
  // 推送
  async function pushSync() {
    if (!$syncUrl) return;
    const url = ($syncUrl.value || '').trim();
    if (!url) { setSyncStatus('未配置同步服务URL'); return; }
    const headers = { 'Content-Type': 'application/json' };
    const authHeader = getAuthHeader();
    if (authHeader) headers['Authorization'] = authHeader;
    const payload = { version: 1, exportedAt: Date.now(), items };
    try {
      setSyncStatus('正在推送...');
      const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setSyncStatus('推送完成');
      announce('已推送到云');
      showToast('推送到云成功', 'success');
    } catch (err) {
      setSyncStatus(`推送失败：${err.message}`);
      // 可选：错误提示
      // showToast(`推送失败：${err.message}`, 'error');
    }
  }
  async function pullSync() {
    if (!$syncUrl) return;
    const url = ($syncUrl.value || '').trim();
    if (!url) { setSyncStatus('未配置同步服务URL'); return; }
    const headers = {};
    const authHeader = getAuthHeader();
    if (authHeader) headers['Authorization'] = authHeader;
    try {
      setSyncStatus('正在拉取...');
      const res = await fetch(url, { method: 'GET', headers });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      const sourceItems = Array.isArray(data) ? data : (Array.isArray(data.items) ? data.items : []);
      const normalized = normalizeItems(sourceItems);
      items = normalized;
      skipNextSyncPush = true; // 拉取后不触发自动推送
      saveItems();
      render();
      setSyncStatus(`拉取完成，共 ${items.length} 条`);
      announce('已从云端拉取');
      showToast('从云拉取成功', 'success');
    } catch (err) {
      setSyncStatus(`拉取失败：${err.message}`);
      // 可选：错误提示
      // showToast(`拉取失败：${err.message}`, 'error');
    }
  }
  function parseDateInput(val) { if (!val) return null; const ts = Date.parse(val); return Number.isFinite(ts) ? ts : null; }
  function fmtDate(ts) { if (!ts && ts !== 0) return ''; try { return new Date(ts).toLocaleString('zh-CN', { hour12: false }); } catch { return ''; } }

  function announce(msg) { $announce.textContent = msg; }
  function showToast(msg, type = 'success') {
    if (!$toastContainer) return;
    const el = document.createElement('div');
    el.className = `toast ${type}`;
    el.textContent = msg;
    $toastContainer.appendChild(el);
    // 触发展示动画
    requestAnimationFrame(() => { el.classList.add('show'); });
    // 3秒后自动隐藏并移除
    setTimeout(() => {
      el.classList.remove('show');
      el.classList.add('hide');
      setTimeout(() => { el.remove(); }, 300);
    }, 3000);
  }

  function addItem(text) {
    const val = (text || '').trim();
    if (!val) return;
    const owner = ($owner.value || '').trim();
    const assignedAt = parseDateInput($assignedAt.value);
    const progress = STATUS.includes($progressSel.value) ? $progressSel.value : '待分配';
    const type = TYPES.includes($typeSel.value) ? $typeSel.value : '日常';

    const newItem = {
      id: uid(), text: val, completed: progress === '已完成', createdAt: Date.now(), owner, assignedAt, progress, type,
    };
    items.unshift(newItem);
    saveItems();
    clearNewInputs();
    render();
    announce('已添加待办');
  }

  function clearNewInputs() { $input.value = ''; $assignedAt.value = ''; $progressSel.value = '待分配'; $typeSel.value = '日常'; }

  function toggleItem(id) {
    const it = items.find(i => i.id === id);
    if (!it) return;
    it.completed = !it.completed;
    it.progress = it.completed ? '已完成' : (it.progress === '已完成' ? '待分配' : it.progress);
    saveItems();
    render();
    announce(it.completed ? '标记为已完成' : '标记为未完成');
  }

  function deleteItem(id) {
    const before = items.length;
    items = items.filter(i => i.id !== id);
    if (items.length !== before) { saveItems(); render(); announce('已删除待办'); }
  }

  function updateItem(id, payload) {
    const it = items.find(i => i.id === id);
    if (!it) return;
    const nextText = (payload.text || '').trim();
    if (!nextText) { deleteItem(id); return; }
    it.text = nextText;
    it.owner = (payload.owner || '').trim();
    it.progress = STATUS.includes(payload.progress) ? payload.progress : it.progress;
    it.type = TYPES.includes(payload.type) ? payload.type : it.type;
    it.completed = it.progress === '已完成';
    saveItems();
    render();
    announce('已更新任务');
  }

  function setFilter(next) {
    currentFilter = next;
    document.querySelectorAll('.filter').forEach(btn => {
      const active = btn.dataset.filter === next;
      btn.classList.toggle('active', active);
      btn.setAttribute('aria-pressed', String(active));
    });
    render();
  }
  function setProgressFilter(next) { progressFilter = next; render(); }
  function setTypeFilter(next) { typeFilter = next; render(); }

  function filteredItems() {
    let data = items.slice();
    if (currentFilter === 'active') data = data.filter(i => !i.completed);
    if (currentFilter === 'completed') data = data.filter(i => i.completed);
    if (progressFilter !== 'all') data = data.filter(i => i.progress === progressFilter);
    if (typeFilter !== 'all') data = data.filter(i => i.type === typeFilter);
    return data;
  }

  function announce(msg) { $announce.textContent = msg; }

  // Drag & Drop reorder
  function onDragStart(e, id) {
    draggedId = id;
    const li = e.target.closest('li');
    if (li) li.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    // some browsers require setData to enable drop
    e.dataTransfer.setData('text/plain', id);
  }
  function onDragOver(e) { e.preventDefault(); const li = e.currentTarget; li.classList.add('drag-over'); e.dataTransfer.dropEffect = 'move'; }
  function onDragLeave(e) { const li = e.currentTarget; li.classList.remove('drag-over'); }
  function onDrop(e, targetId) {
    e.preventDefault();
    const li = e.currentTarget; li.classList.remove('drag-over');
    const srcId = e.dataTransfer.getData('text/plain') || draggedId;
    if (!srcId || srcId === targetId) return;
    const from = items.findIndex(i => i.id === srcId);
    const to = items.findIndex(i => i.id === targetId);
    if (from === -1 || to === -1) return;
    const [moved] = items.splice(from, 1);
    items.splice(to, 0, moved);
    saveItems();
    render();
    announce('已调整顺序');
    draggedId = null;
  }
  function onDragEnd(e) { const li = e.target.closest('li'); if (li) li.classList.remove('dragging'); draggedId = null; }

  function render() {
    const data = filteredItems();
    $list.innerHTML = '';
    for (const it of data) {
      const li = document.createElement('li');
      li.className = 'todo-item';
      li.dataset.id = it.id;

      const handle = document.createElement('button');
      handle.className = 'drag-handle';
      handle.setAttribute('aria-label', '拖拽以调整顺序');
      handle.textContent = '⠿';
      handle.draggable = true;

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.className = 'toggle';
      checkbox.checked = it.completed;
      checkbox.setAttribute('aria-label', it.completed ? '取消完成' : '标记完成');

      const display = document.createElement('div');
      display.className = 'display';
      const title = document.createElement('span');
      title.className = 'todo-text' + (it.completed ? ' completed' : '');
      title.textContent = it.text;
      title.title = '双击进行编辑';

      const meta = document.createElement('div');
      meta.className = 'item-meta';
      const bProgress = document.createElement('span');
      bProgress.className = 'badge progress';
      bProgress.textContent = `进度：${it.progress}`;
      const bType = document.createElement('span');
      bType.className = 'badge type';
      bType.textContent = `类型：${it.type}`;
      const info = document.createElement('div');
      info.textContent = `${it.owner ? '担当：' + it.owner + ' · ' : ''}创建时间：${fmtDate(it.createdAt)}${it.assignedAt ? ' · 下发时间：' + fmtDate(it.assignedAt) : ''}`;
      meta.appendChild(bProgress);
      meta.appendChild(bType);
      meta.appendChild(info);
      display.appendChild(title);
      display.appendChild(meta);

      const editPanel = document.createElement('div');
      editPanel.className = 'edit-panel';
      const row = document.createElement('div');
      row.className = 'row';
      const editText = document.createElement('textarea');
      editText.rows = 6;
      editText.className = 'wide';
      editText.value = it.text;
      editText.placeholder = '任务内容';
      const editOwner = document.createElement('input');
      editOwner.type = 'text';
      editOwner.className = 'medium';
      editOwner.value = it.owner || '';
      editOwner.placeholder = '担当（负责人）';
      const editProgress = document.createElement('select');
      editProgress.className = 'small';
      for (const s of STATUS) { const o = document.createElement('option'); o.value = s; o.textContent = s; editProgress.appendChild(o); }
      editProgress.value = it.progress;
      const editType = document.createElement('select');
      editType.className = 'small';
      for (const t of TYPES) { const o = document.createElement('option'); o.value = t; o.textContent = t; editType.appendChild(o); }
      editType.value = it.type;
      row.appendChild(editText);
      row.appendChild(editOwner);
      row.appendChild(editProgress);
      row.appendChild(editType);
      const actions = document.createElement('div');
      actions.className = 'edit-actions';
      const btnSave = document.createElement('button');
      btnSave.className = 'save';
      btnSave.textContent = '保存';
      const btnCancel = document.createElement('button');
      btnCancel.className = 'cancel';
      btnCancel.textContent = '取消';
      actions.appendChild(btnSave);
      actions.appendChild(btnCancel);
      editPanel.appendChild(row);
      editPanel.appendChild(actions);

      const editToggle = document.createElement('button');
      editToggle.className = 'edit-btn';
      editToggle.textContent = '编辑';

      const del = document.createElement('button');
      del.className = 'delete-btn';
      del.innerHTML = '&times;';
      del.setAttribute('aria-label', '删除');

      li.appendChild(handle);
      li.appendChild(checkbox);
      li.appendChild(display);
      li.appendChild(editPanel);
      li.appendChild(editToggle);
      li.appendChild(del);
      $list.appendChild(li);

      // Events
      handle.addEventListener('dragstart', (e) => onDragStart(e, it.id));
      handle.addEventListener('dragend', onDragEnd);
      li.addEventListener('dragover', onDragOver);
      li.addEventListener('dragleave', onDragLeave);
      li.addEventListener('drop', (e) => onDrop(e, it.id));

      checkbox.addEventListener('change', () => {
        const willLeaveActive = (currentFilter === 'active' && checkbox.checked);
        const willLeaveCompleted = (currentFilter === 'completed' && !checkbox.checked);
        if (willLeaveActive || willLeaveCompleted) {
          // 平滑淡出后再真正切换状态并重渲染
          li.classList.add('leaving');
          let finished = false;
          const finalize = () => {
            if (finished) return; // 防止重复触发导致状态反复切换
            finished = true;
            toggleItem(it.id);
          };
          // 监听一次过渡结束，避免多次触发
          li.addEventListener('transitionend', finalize, { once: true });
          // 兜底：部分环境可能不触发transitionend
          setTimeout(() => finalize(), 320);
        } else {
          toggleItem(it.id);
        }
      });
      del.addEventListener('click', () => deleteItem(it.id));
      title.addEventListener('dblclick', () => { li.classList.add('editing'); editText.focus(); editText.setSelectionRange(editText.value.length, editText.value.length); });
      editToggle.addEventListener('click', () => { li.classList.add('editing'); editText.focus(); });
      btnCancel.addEventListener('click', () => { li.classList.remove('editing'); });
      btnSave.addEventListener('click', () => updateItem(it.id, { text: editText.value, owner: editOwner.value, progress: editProgress.value, type: editType.value }));
      editText.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); btnSave.click(); }
      if (e.key === 'Escape') btnCancel.click();
      });
      // 失焦自动保存（仅当焦点离开编辑面板整体时）
      editPanel.addEventListener('focusout', (e) => {
        const to = e.relatedTarget;
        if (to && editPanel.contains(to)) return; // 在面板内切换焦点不保存
        const blocked = autoSaveBlock.has(it.id);
        autoSaveBlock.delete(it.id);
        if (blocked) return; // 取消按钮触发的失焦，不保存
        updateItem(it.id, { text: editText.value, owner: editOwner.value, progress: editProgress.value, type: editType.value });
        li.classList.remove('editing');
      });
      // 选择器变更即保存
      editProgress.addEventListener('change', () => {
        updateItem(it.id, { text: editText.value, owner: editOwner.value, progress: editProgress.value, type: editType.value });
      });
      editType.addEventListener('change', () => {
        updateItem(it.id, { text: editText.value, owner: editOwner.value, progress: editProgress.value, type: editType.value });
      });
      // 防止取消导致失焦自动保存
      btnCancel.addEventListener('mousedown', () => { autoSaveBlock.add(it.id); });
    }

    const left = items.filter(i => !i.completed).length;
    $itemsLeft.textContent = `${left} 条未完成`;
    $clearCompleted.disabled = !items.some(i => i.completed);
  }

  function exportAll() {
    const payload = { version: 1, exportedAt: Date.now(), items };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    const ts = new Date().toISOString().replace(/[:.]/g, '-');
    a.download = `todos-${ts}.json`;
    a.href = URL.createObjectURL(blob);
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 1000);
    announce('已导出所有任务');
  }

  async function importAll(file) {
    if (!file) return;
    try {
      const text = await file.text();
      const data = JSON.parse(text);
      const sourceItems = Array.isArray(data) ? data : (Array.isArray(data.items) ? data.items : []);
      const normalized = sourceItems.map(i => ({
        id: i.id || uid(),
        text: i.text || '',
        completed: !!i.completed || i.progress === '已完成',
        createdAt: i.createdAt || Date.now(),
        owner: i.owner || '',
        assignedAt: i.assignedAt ?? null,
        progress: STATUS.includes(i.progress) ? i.progress : (i.completed ? '已完成' : '待分配'),
        type: TYPES.includes(i.type) ? i.type : '日常',
      }));
      items = normalized;
      saveItems();
      render();
      announce(`已导入 ${items.length} 条任务`);
    } catch {
      announce('导入失败：文件格式错误');
    }
  }

  $addBtn.addEventListener('click', () => addItem($input.value));
  $input.addEventListener('keydown', (e) => { if (e.key === 'Enter') addItem($input.value); });
  $filters.forEach(btn => btn.addEventListener('click', () => setFilter(btn.dataset.filter)));
  $progressFilter.addEventListener('change', () => setProgressFilter($progressFilter.value));
  $typeFilter.addEventListener('change', () => setTypeFilter($typeFilter.value));
  $clearCompleted.addEventListener('click', () => clearCompleted());
  $exportBtn.addEventListener('click', exportAll);
  $importBtn.addEventListener('click', () => $importFile.click());
  $importFile.addEventListener('change', () => importAll($importFile.files[0]));
  if ($backupRefresh) $backupRefresh.addEventListener('click', () => renderBackups());
  if ($backupWriteNow) $backupWriteNow.addEventListener('click', async () => { await writeBackupSnapshot(true); renderBackups(); });
  if ($syncUrl) $syncUrl.addEventListener('change', saveSyncConfig);
  if ($syncToken) $syncToken.addEventListener('change', saveSyncConfig);
  if ($syncAuto) $syncAuto.addEventListener('change', saveSyncConfig);
  if ($syncPush) $syncPush.addEventListener('click', () => { if (!getAuthHeader()) { showLogin(); return; } pushSync(); });
  if ($syncPull) $syncPull.addEventListener('click', () => { if (!getAuthHeader()) { showLogin(); return; } pullSync(); });

  await initStorage();
  items = await loadItemsAsync();
  loadSyncConfig();
  render();
  // 取消自动备份：不再调用 scheduleAutoBackup()
  renderBackups();
})();